package cn.iocoder.ny.module.mall.utils.wxpay;

import cn.hutool.core.io.IoUtil;
import cn.iocoder.ny.module.mall.domain.MallConfig;
import com.alibaba.fastjson.JSON;

import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.exception.HttpException;
import com.wechat.pay.java.core.exception.MalformedMessageException;
import com.wechat.pay.java.core.exception.ServiceException;
import com.wechat.pay.java.core.exception.ValidationException;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.core.util.GsonUtil;
import com.wechat.pay.java.service.payments.app.AppServiceExtension;
import com.wechat.pay.java.service.payments.h5.H5Service;
import com.wechat.pay.java.service.payments.h5.model.H5Info;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import com.wechat.pay.java.service.payments.jsapi.model.Amount;
import com.wechat.pay.java.service.payments.jsapi.model.*;
import com.wechat.pay.java.service.payments.model.Transaction;
import com.wechat.pay.java.service.payments.nativepay.NativePayService;
import com.wechat.pay.java.service.refund.RefundService;
import com.wechat.pay.java.service.refund.model.*;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.http.HttpServletRequest;

/**
 * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/index.shtml
 * https://github.com/wechatpay-apiv3/wechatpay-java
 * https://hub.fgit.ml/wechatpay-apiv3/wechatpay-java
 */
@Slf4j
@Data
public class WxPayUtil {


    private String appId; // 小程序的appid
    private String appSecret; // 小程序的密钥
    private String mchId; //商户id
    private String mchSerialNumber; //商户序列号
    private String apiV3Key; // apiV3Key
    private String notifyUrl; //回调通知URL地址 必须是https协议
    private String privateKey; //商户私钥路径
    private RSAAutoCertificateConfig config;


    public WxPayUtil(MallConfig mallConfig){
        this.appId=mallConfig.getAppid();
        this.appSecret=mallConfig.getSecret();
        this.mchId=mallConfig.getMchId();
        this.mchSerialNumber=mallConfig.getMchSerialNumber();
        this.privateKey=mallConfig.getPrivateKey();
        this.apiV3Key=mallConfig.getApiV3Key();
        this.config=new RSAAutoCertificateConfig.Builder()
                .merchantId(mallConfig.getMchId())
                .privateKey(mallConfig.getPrivateKey())
                .merchantSerialNumber(mallConfig.getMchSerialNumber())
                .apiV3Key(mallConfig.getApiV3Key())
                .build();
    }


    /**
     * JSAPI下单(小程序支付)
     * 商户通过调用微信支付提供的JSAPI接口，在支付场景中调起微信支付模块完成收款。
     * https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter1_1_1.shtml
     *
     * @param description 商品描述
     * @param outTradeNo  商户订单号
     * @param attach      附加数据
     * @param openid      用户标识
     * @param total       金额
     * @param notifyUrl 回调通知URL地址
     */
    public  ResultModel<PrepayWithRequestPaymentResponse> prepayByJsapi(String description, String outTradeNo, String attach, String openid, Integer total,String notifyUrl) {
        log.info("JSAPI下单-开始");

        ResultModel<PrepayWithRequestPaymentResponse> resultModel = new ResultModel<>();
        // 构建service
        JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(config).build();
        //构建请求
        PrepayRequest request = new PrepayRequest();
        //基础信息
        request.setAppid(appId);
        request.setMchid(mchId);
        request.setDescription(description);//商品描述
        request.setOutTradeNo(outTradeNo);//商户系统内部订单号
        request.setAttach(attach);//附加数据
        request.setNotifyUrl(notifyUrl);
        //订单金额信息
        Amount amount = new Amount();
        amount.setTotal(total);
        request.setAmount(amount);
        //用户在直连商户appid下的唯一标识。 下单前需获取到用户的Openid
        Payer payer = new Payer();
        payer.setOpenid(openid);
        request.setPayer(payer);
        try {
            PrepayWithRequestPaymentResponse response = service.prepayWithRequestPayment(request);
            log.info("JSAPI下单-结果：{}", response.toString());
            resultModel.setIsSuccess(true);
            resultModel.setData(response);
        } catch (Exception ex) {
            setException(resultModel, ex);
        }
        log.info("JSAPI下单-结束：{}", JSON.toJSONString(resultModel));
        return resultModel;
    }

    /**
     * APP下单
     * 商户通过调用微信支付提供的JSAPI接口，在支付场景中调起微信支付模块完成收款。
     * https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter1_1_1.shtml
     *
     * @param description   商品描述
     * @param outTradeNo    商户订单号
     * @param attach    附加数据
     * @param total 总金额
     * @return
     */
    public  ResultModel<com.wechat.pay.java.service.payments.app.model.PrepayWithRequestPaymentResponse> prepayByApp(String description, String outTradeNo, String attach, Integer total,String notifyUrl) {
        log.info("APP下单-开始");

        ResultModel<com.wechat.pay.java.service.payments.app.model.PrepayWithRequestPaymentResponse> resultModel = new ResultModel<>();
        // 构建service
        AppServiceExtension service = new AppServiceExtension.Builder().config(config).build();
        //构建请求
        com.wechat.pay.java.service.payments.app.model.PrepayRequest request = new com.wechat.pay.java.service.payments.app.model.PrepayRequest();
        //基础信息
        request.setAppid(appId);
        request.setMchid(mchId);
        request.setDescription(description);//商品描述
        request.setOutTradeNo(outTradeNo);//商户系统内部订单号
        request.setAttach(attach);//附加数据
        request.setNotifyUrl(notifyUrl);
        //订单金额信息
        com.wechat.pay.java.service.payments.app.model.Amount amount = new com.wechat.pay.java.service.payments.app.model.Amount();
        amount.setTotal(total);
        request.setAmount(amount);
        try {
            com.wechat.pay.java.service.payments.app.model.PrepayWithRequestPaymentResponse response = service.prepayWithRequestPayment(request);
            log.info("APP下单-结果：{}", response.toString());
            resultModel.setIsSuccess(true);
            resultModel.setData(response);
        } catch (Exception ex) {
            setException(resultModel, ex);
        }
        log.info("APP下单-结束：{}",JSON.toJSONString(resultModel));
        return resultModel;
    }

    /**
     * H5下单
     * 商户系统先调用该接口在微信支付服务后台生成预支付交易单，返回正确的预支付交易会话标识后再按Native、JSAPI、APP等不同场景生成交易串调起支付。
     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_3_1.shtml
     *
     * @param description   商品描述
     * @param outTradeNo    商户订单号
     * @param attach    附加数据
     * @param total 总金额
     * @param payerClientIp 用户终端IP
     * @return
     */
    public  ResultModel<com.wechat.pay.java.service.payments.h5.model.PrepayResponse> prepayByH5(String description, String outTradeNo, String attach, Integer total,String payerClientIp,String notifyUrl) {
        log.info("H5下单-开始");

        ResultModel<com.wechat.pay.java.service.payments.h5.model.PrepayResponse> resultModel = new ResultModel<>();
        // 构建service
        H5Service service = new H5Service.Builder().config(config).build();
        //构建请求
        com.wechat.pay.java.service.payments.h5.model.PrepayRequest request = new com.wechat.pay.java.service.payments.h5.model.PrepayRequest();
        //基础信息
        request.setAppid(appId);
        request.setMchid(mchId);
        request.setDescription(description);//商品描述
        request.setOutTradeNo(outTradeNo);//商户系统内部订单号
        request.setAttach(attach);//附加数据
        request.setNotifyUrl(notifyUrl);
        //订单金额信息
        com.wechat.pay.java.service.payments.h5.model.Amount amount = new com.wechat.pay.java.service.payments.h5.model.Amount();
        amount.setTotal(total);
        request.setAmount(amount);
        //场景信息
        com.wechat.pay.java.service.payments.h5.model.SceneInfo sceneInfo = new com.wechat.pay.java.service.payments.h5.model.SceneInfo();
        sceneInfo.setPayerClientIp(payerClientIp);
        //H5场景信息
        H5Info h5Info = new H5Info();
        h5Info.setType("Wap");
        sceneInfo.setH5Info(h5Info);
        request.setSceneInfo(sceneInfo);

        try {
            com.wechat.pay.java.service.payments.h5.model.PrepayResponse response = service.prepay(request);
            log.info("H5下单-结果：{}", response.toString());
            resultModel.setIsSuccess(true);
            resultModel.setData(response);
        } catch (Exception ex) {
            setException(resultModel, ex);
        }
        log.info("H5下单-结束：{}",JSON.toJSONString(resultModel));
        return resultModel;
    }

    /**
     * Native下单(扫码支付)
     * 商户系统按微信支付协议生成支付二维码，用户再用微信“扫一扫”完成支付的模式
     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_3_1.shtml
     *
     * @param description   商品描述
     * @param outTradeNo    商户订单号
     * @param attach    附加数据
     * @param total 总金额
     * @return
     */
    public  ResultModel<com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse> prepayByNative(String description, String outTradeNo, String attach, Integer total,String notifyUrl) {
        log.info("Native下单(扫码支付)-开始");

        ResultModel<com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse> resultModel = new ResultModel<>();
        // 构建service
        NativePayService service = new NativePayService.Builder().config(config).build();
        //构建请求
        com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest request = new com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest();
        //基础信息
        request.setAppid(appId);
        request.setMchid(mchId);
        request.setDescription(description);//商品描述
        request.setOutTradeNo(outTradeNo);//商户系统内部订单号
        request.setAttach(attach);//附加数据
        request.setNotifyUrl(notifyUrl);
        //订单金额信息
        com.wechat.pay.java.service.payments.nativepay.model.Amount amount = new com.wechat.pay.java.service.payments.nativepay.model.Amount();
        amount.setTotal(total);
        request.setAmount(amount);

        try {
            com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse response = service.prepay(request);
            log.info("Native下单(扫码支付)-结果：{}", response.toString());
            resultModel.setIsSuccess(true);
            resultModel.setData(response);
        } catch (Exception ex) {
            setException(resultModel, ex);
        }
        log.info("Native下单(扫码支付)-结束：{}",JSON.toJSONString(resultModel));
        return resultModel;
    }

    /**
     * 查询订单-商户订单号查询
     * 商户可以通过查询订单接口主动查询订单状态，完成下一步的业务逻辑。查询订单状态可通过微信支付订单号或商户订单号两种方式查询
     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_2.shtml
     *
     * @param outTradeNo 商户系统内部订单号，只能是数字、大小写字母_-*且在同一个商户号下唯一。
     * @return
     */
    public  ResultModel<Transaction> queryByOutTradeNo(String outTradeNo) {
        log.info("查询订单-商户订单号查询-开始");

        ResultModel<Transaction> resultModel = new ResultModel<>();
        // 构建service
        JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(config).build();
        //构建请求
        QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
        request.setMchid(mchId);
        request.setOutTradeNo(outTradeNo);
        try {
            Transaction transaction = service.queryOrderByOutTradeNo(request);
            resultModel.setIsSuccess(true);
            resultModel.setData(transaction);
        } catch (Exception ex) {
            setException(resultModel, ex);
        }
        log.info("查询订单-商户订单号查询-结束：{}",JSON.toJSONString(resultModel));
        return resultModel;
    }

    /**
     * 查询订单-微信支付订单号查询
     * 商户可以通过查询订单接口主动查询订单状态，完成下一步的业务逻辑。查询订单状态可通过微信支付订单号或商户订单号两种方式查询
     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_2.shtml
     *
     * @param transactionId 微信支付订单号
     * @return
     */
    public  ResultModel<Transaction> queryByTransactionId(String transactionId) {
        log.info("查询订单-微信支付订单号查询-开始");

        ResultModel<Transaction> resultModel = new ResultModel<>();
        // 构建service
        JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(config).build();
        //构建请求
        QueryOrderByIdRequest request = new QueryOrderByIdRequest();
        request.setMchid(mchId);
        request.setTransactionId(transactionId);
        try {
            Transaction transaction = service.queryOrderById(request);
            resultModel.setIsSuccess(true);
            resultModel.setData(transaction);
        } catch (Exception ex) {
            setException(resultModel, ex);
        }
        log.info("查询订单-微信支付订单号查询-结束：{}",JSON.toJSONString(resultModel));
        return resultModel;
    }

    /**
     * 关闭订单
     * 以下情况需要调用关单接口：
     * 1、商户订单支付失败需要生成新单号重新发起支付，要对原订单号调用关单，避免重复支付；
     * 2、系统下单后，用户支付超时，系统退出不再受理，避免用户继续，请调用关单接口。
     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_3.shtml
     *
     * @param outTradeNo 商户系统内部订单号，只能是数字、大小写字母_-*且在同一个商户号下唯一
     * @return
     */
    public  ResultModel<Transaction> closeByOutTradeNo(String outTradeNo) {
        log.info("关闭订单-开始");

        ResultModel<Transaction> resultModel = new ResultModel<>();
        // 构建service
        JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(config).build();
        //构建请求
        CloseOrderRequest request = new CloseOrderRequest();
        request.setMchid(mchId);
        request.setOutTradeNo(outTradeNo);
        try {
            service.closeOrder(request);
            resultModel.setIsSuccess(true);
        } catch (Exception ex) {
            setException(resultModel, ex);
        }
        log.info("关闭订单-结束：{}", JSON.toJSONString(resultModel));
        return resultModel;
    }

    /**
     * 申请退款-商户订单号
     * 当交易发生之后一年内，由于买家或者卖家的原因需要退款时，卖家可以通过退款接口将支付金额退还给买家，微信支付将在收到退款请求并且验证成功之后，将支付款按原路退还至买家账号上。
     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_9.shtml
     *
     * @param outTradeNo  商户订单号
     * @param outRefundNo 商户退款单号
     * @param total       原订单金额
     * @param refund      退款金额
     * @return
     */
    public  ResultModel<Refund> refundsByOutTradeNo(String outTradeNo, String outRefundNo, Long total, Long refund,String refundNotifyUrl) {
        log.info("申请退款-开始");

        ResultModel<Refund> resultModel = new ResultModel<>();
        // 构建service
        RefundService service = new RefundService.Builder().config(config).build();

        //构建请求
        CreateRequest request = new CreateRequest();
        request.setOutTradeNo(outTradeNo);
        request.setOutRefundNo(outRefundNo);
        request.setNotifyUrl(refundNotifyUrl);
        //金额信息
        AmountReq amountReq = new AmountReq();
        amountReq.setRefund(refund);
        amountReq.setTotal(total);
        amountReq.setCurrency("CNY");
        request.setAmount(amountReq);

        try {
            service.create(request);
            resultModel.setIsSuccess(true);
        } catch (Exception ex) {
            setException(resultModel, ex);
        }
        log.info("申请退款-结束：{}-{}",JSON.toJSONString(resultModel),refundNotifyUrl);
        return resultModel;
    }

    /**
     * 申请退款-微信支付订单号
     * 当交易发生之后一年内，由于买家或者卖家的原因需要退款时，卖家可以通过退款接口将支付金额退还给买家，微信支付将在收到退款请求并且验证成功之后，将支付款按原路退还至买家账号上。
     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_9.shtml
     *
     * @param transactionId 微信支付订单号
     * @param outRefundNo   商户退款单号
     * @param total         原订单金额
     * @param refund        退款金额
     * @return
     */
    public  ResultModel<Refund> refundsByTransactionId(String transactionId, String outRefundNo, Integer total, Integer refund,String refundNotifyUrl) {
        log.info("申请退款-开始");

        ResultModel<Refund> resultModel = new ResultModel<>();
        // 构建service
        RefundService service = new RefundService.Builder().config(config).build();

        //构建请求
        CreateRequest request = new CreateRequest();
        request.setTransactionId(transactionId);
        request.setOutRefundNo(outRefundNo);
        request.setNotifyUrl(refundNotifyUrl);
        //金额信息
        AmountReq amountReq = new AmountReq();
        amountReq.setRefund(refund + 0l);
        amountReq.setTotal(total + 0l);
        amountReq.setCurrency("CNY");
        request.setAmount(amountReq);

        try {
            service.create(request);
            resultModel.setIsSuccess(true);
        } catch (Exception ex) {
            setException(resultModel, ex);
        }
        log.info("申请退款-结束：{}",JSON.toJSONString(resultModel));
        return resultModel;
    }

    /**
     * 查询单笔退款
     * 提交退款申请后，通过调用该接口查询退款状态。退款有一定延时，建议在提交退款申请后1分钟发起查询退款状态，一般来说零钱支付的退款5分钟内到账，银行卡支付的退款1-3个工作日到账。
     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_10.shtml
     *
     * @param outRefundNo 商户退款单号
     * @return
     */
    public  ResultModel<Refund> refundsQueryByOutRefundNo(String outRefundNo) {
        log.info("查询单笔退款-开始");

        ResultModel<Refund> resultModel = new ResultModel<>();
        // 构建service
        RefundService service = new RefundService.Builder().config(config).build();
        //构建请求
        QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest();
        request.setOutRefundNo(outRefundNo);
        try {
            Refund transaction = service.queryByOutRefundNo(request);
            resultModel.setIsSuccess(true);
            resultModel.setData(transaction);
        } catch (Exception ex) {
            setException(resultModel, ex);
        }
        log.info("查询单笔退款-结束：{}",JSON.toJSONString(resultModel));
        return resultModel;
    }

    /**
     * 支付通知
     * 微信支付通过支付通知接口将用户支付成功消息通知给商户
     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_10.shtml
     *
     * @return
     */
    public  ResultModel<Transaction> notify(HttpServletRequest request) {
        log.info("支付通知-开始");
        ResultModel<Transaction> resultModel = new ResultModel<>();

        try {
            String body = IoUtil.getUtf8Reader(request.getInputStream()).readLine();
            RequestParam requestParam = new RequestParam.Builder()
                    .serialNumber(request.getHeader("Wechatpay-Serial"))
                    .nonce(request.getHeader("Wechatpay-Nonce"))
                    .signature(request.getHeader("Wechatpay-Signature"))
                    .timestamp(request.getHeader("Wechatpay-Timestamp"))
                    .signType(request.getHeader("Wechatpay-Signature-Type"))
                    .body(body)
                    .build();
            NotificationParser parser = new NotificationParser(config);
            // 以支付通知回调为例，验签、解密并转换成 Transaction
            Transaction transaction = parser.parse(requestParam, Transaction.class);
            resultModel.setIsSuccess(true);
            resultModel.setData(transaction);
        } catch (Exception ex) {
            setException(resultModel, ex);
        }
        log.info("支付通知-结束：{}", GsonUtil.getGson().toJson(resultModel));
        return resultModel;
    }

    /**
     * 退款结果通知
     * 退款状态改变后，微信会把相关退款结果发送给商户。
     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_11.shtml
     *
     * @return
     */
    public  ResultModel<RefundNotification> refundNotify(HttpServletRequest request) {
        log.info("退款结果通知-开始");
        ResultModel<RefundNotification> resultModel = new ResultModel<>();

        try {
            String body = IoUtil.getUtf8Reader(request.getInputStream()).readLine();
            RequestParam requestParam = new RequestParam.Builder()
                    .serialNumber(request.getHeader("Wechatpay-Serial"))
                    .nonce(request.getHeader("Wechatpay-Nonce"))
                    .signature(request.getHeader("Wechatpay-Signature"))
                    .timestamp(request.getHeader("Wechatpay-Timestamp"))
                    .signType(request.getHeader("Wechatpay-Signature-Type"))
                    .body(body)
                    .build();
            NotificationParser parser = new NotificationParser(config);
            // 以支付通知回调为例，验签、解密并转换成 Transaction
            RefundNotification refundNotification = parser.parse(requestParam, RefundNotification.class);
            resultModel.setIsSuccess(true);
            resultModel.setData(refundNotification);
        } catch (Exception ex) {
            setException(resultModel, ex);
        }
        log.info("退款结果通知-结束：{}", GsonUtil.getGson().toJson(resultModel));
        return resultModel;
    }

    /**
     * 设置异常
     *
     * @param resultModel
     * @param ex
     */
    private  void setException(ResultModel resultModel, Exception ex) {
        if (ex instanceof HttpException) {
            //调用微信支付服务，当发生 HTTP 请求异常时抛出该异常。
            resultModel.setIsSuccess(false);
            resultModel.setMessage(ex.getMessage());
        } else if (ex instanceof ValidationException) {
            //当验证微信支付签名失败时抛出该异常。
            resultModel.setIsSuccess(false);
            resultModel.setMessage(ex.getMessage());
        } else if (ex instanceof ServiceException) {
            //调用微信支付服务，发送 HTTP 请求成功，HTTP 状态码小于200或大于等于300。
            ServiceException serviceException = (ServiceException) ex;
            resultModel.setIsSuccess(false);
            resultModel.setMessage(serviceException.getErrorMessage());
            resultModel.setCode(serviceException.getErrorCode());
        } else if (ex instanceof MalformedMessageException) {
            //服务返回成功，返回内容异常
            resultModel.setIsSuccess(false);
            resultModel.setMessage(ex.getMessage());
        } else {
            resultModel.setIsSuccess(false);
            resultModel.setMessage(ex.getMessage());
        }
    }
}
